/// Copyright (c) 2021 Razeware LLC
/// 
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
/// 
/// The above copyright notice and this permission notice shall be included in
/// all copies or substantial portions of the Software.
/// 
/// Notwithstanding the foregoing, you may not use, copy, modify, merge, publish,
/// distribute, sublicense, create a derivative work, and/or sell copies of the
/// Software in any work that is designed, intended, or marketed for pedagogical or
/// instructional purposes related to programming, coding, application development,
/// or information technology.  Permission for such use, copying, modification,
/// merger, publication, distribution, sublicensing, creation of derivative works,
/// or sale is expressly withheld.
/// 
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
/// THE SOFTWARE.

@testable import DogPatch
import XCTest

class DogPatchClientTests: XCTestCase{
  var sut: DogPatchClient!
  var baseURL: URL!
  var mockSession: MockURLSession!
  
  override func setUp() {
    super.setUp()
    baseURL = URL.init(string: "https://www.example.com/v1/")!
    mockSession = MockURLSession.init()
    sut = DogPatchClient.init(baseURL: baseURL, session: mockSession)
  }
  
  override func tearDown() {
    
  }
  
  func whenGetDogs(data: Data? = nil, statusCode: Int = 200, error: Error? = nil) ->
  (calledCompletion: Bool, dogs: [Dog]?, error: Error?){
    var calledCompletion: Bool = false
    var receivedDogs: [Dog]?
    var receivedError: Error?
    
    let task = sut.getDog { (dogs, error) in
      calledCompletion = true
      receivedDogs = dogs
      receivedError = error
    } as! MockURLSessionDataTask
    let response = HTTPURLResponse(url: getDosURL(), statusCode: statusCode, httpVersion: nil, headerFields: nil)
    task.completionHandler(data, response, error)
    
    return (calledCompletion, receivedDogs, receivedError)
  }
  
  
  func test_inits_set_baseurl(){
    XCTAssertEqual(sut.baseURL, baseURL)
  }
  
  func test_inits_set_session(){
    XCTAssertEqual(sut.session, mockSession)
  }
  
  func test_getDogs_callsExpectedURL(){
    let url = URL.init(string: "dogs", relativeTo: baseURL)
    let task = sut.getDog { (dogs, error) in
      
    } as! MockURLSessionDataTask
    XCTAssertEqual(task.url, url)
  }
  
  func test_getDogs_calledResumeOnTask(){
    let task = sut.getDog { (dogs, error) in
      
    } as! MockURLSessionDataTask
    XCTAssertTrue(task.calledResumed)
  }
  
  func test_getDogs_givenResponseStatusCode500_callsCompletion(){
    let result = whenGetDogs(data: nil, statusCode: 500, error: nil)
    
    XCTAssertNil(result.dogs)
    XCTAssertNil(result.error)
    XCTAssertTrue(result.calledCompletion)
    
  }
  
  func getDosURL() -> URL{
    return URL.init(string: "dogs", relativeTo: baseURL)!
  }
  
  func test_getDogs_givenNetworkError_callsCompletion(){
//    let url = URL.init(string: "dogs", relativeTo: baseURL)!
    let networkError = NSError.init(domain: "com.dogpatch", code: 42, userInfo: nil)
    
    let result = whenGetDogs(data: nil, statusCode: 200, error: networkError)
    
    XCTAssertNil(result.dogs)
    XCTAssertTrue(result.calledCompletion)
    XCTAssertEqual(result.error as NSError?, networkError)
  }
  
  func test_getDogs_givenValidJSON_callsCompletionWithDogs() throws {
    let data = try! Data.fromJSON(fileName: "GET_Dogs_Response")
    let dogs = try! JSONDecoder.init().decode([Dog].self, from: data)
    let result = whenGetDogs(data: data, statusCode: 200, error: nil)
    XCTAssertTrue(result.calledCompletion)
    XCTAssertEqual(result.dogs, dogs)
    XCTAssertNil(result.error)
  }
  
  func test_getDogs_givenInvalidJSON_callsCompletionWithError() throws {
    // given
    let data = try Data.fromJSON(fileName: "GET_Dogs_MissingValuesResponse")
    var dogs: [Dog]?
    var expectedError: NSError?
    do{
      let decoder = JSONDecoder.init()
      dogs = try decoder.decode([Dog].self, from: data)
    }catch{
      expectedError = error as NSError
    }
    
    let result = whenGetDogs(data: data, statusCode: 200, error: nil)
    
    XCTAssertNil(dogs)
    let actualError = try XCTUnwrap(result.error as NSError?)
    XCTAssertEqual(actualError.domain, expectedError?.domain)
    XCTAssertEqual(actualError.code, expectedError?.code)
  }
  
  func test_init_sets_responseQueue(){
    let sut = DogPatchClient(baseURL: getDosURL(), session: URLSession.shared, responseQueue: DispatchQueue.main)
    XCTAssertEqual(sut.responseQueue, DispatchQueue.main)
  }
  
  func test_mockSession_givenDispatchQueue_responseNotInMainQueue(){
    let mockSession = MockURLSession()
    var thread: Thread?
    mockSession.givenDispatchQueue()
    let exp = expectation(description: "test_mockSession_givenDispatchQueue_notInMainQueue")
    let mockTask = mockSession.dataTask(with: getDosURL()) { (_, _, _) in
      thread = Thread.current
      exp.fulfill()
    } as! MockURLSessionDataTask
    mockTask.resume()
    mockTask.completionHandler(nil, nil, nil)
    wait(for: [exp], timeout: 0.1)
    XCTAssertNotEqual(thread, Thread.main)
  }
  
  func test_getDogs_givenHTTPStatusError_dispatchesToResponseQueue() {
    mockSession.givenDispatchQueue()
    let sut = DogPatchClient(baseURL: getDosURL(), session: mockSession, responseQueue: DispatchQueue.main)
    let exp = expectation(description: "test for Dispatch Queue")
    let response = HTTPURLResponse.init(url: getDosURL(), statusCode: 500, httpVersion: nil, headerFields: nil)
    var thread: Thread?
    var receivedDogs: [Dog]?
    var receivedError: Error?
    
    let task = sut.getDog { (dog, error) in
      thread = Thread.current
      receivedDogs = dog
      receivedError = error
      exp.fulfill()
    } as! MockURLSessionDataTask
    task.completionHandler(nil, response, nil)
    
    wait(for: [exp], timeout: 0.1)
    XCTAssertEqual(thread, Thread.main)
    XCTAssertNil(receivedDogs)
    XCTAssertNil(receivedError)
  }
  
}

class MockURLSession: URLSession{
  var queue: DispatchQueue?
  
  func givenDispatchQueue(){
    queue = DispatchQueue.init(label: "com.DogPatchTests.MockSession")
  }
  
  override func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
    return MockURLSessionDataTask(url: url, completionHandler: completionHandler, queue: queue)
  }
}

class MockURLSessionDataTask: URLSessionDataTask{
  let url: URL
  let completionHandler: ((Data?, URLResponse?, Error?) -> Void)
  var calledResumed: Bool = false
  
  
  init(url: URL, completionHandler: @escaping ((Data?, URLResponse?, Error?) -> Void),
       queue: DispatchQueue? = nil){
    self.url = url
    if let queue = queue {
      self.completionHandler = { data, response, error in
        queue.async {
          completionHandler(data, response, error)
        }
      }
    }else{
      self.completionHandler = completionHandler
    }
    
  }
  
  override func resume() {
    calledResumed = true
  }
}

